/*
 * MIT License
 *
 * Copyright (c) 2020 wen.gu <454727014@qq.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

 /***************************************************************************
 * Name: ini_file.cpp
 *
 * Purpose: ini file parse
 *
 * Developer:
 *   wen.gu , 2019-07-03
 *
 * TODO:
 *
 ***************************************************************************/

 /******************************************************************************
 **    INCLUDES
 ******************************************************************************/

#include "collie/core/ini_file.h"

#include <errno.h>
#include <stdio.h>
#include <ctype.h>
#include <sstream>
#include <string.h>

#define LOG_TAG "IniFile"
#include "collie/core/log.h"

namespace collie
{
namespace core
{
/******************************************************************************
 **    MACROS
 ******************************************************************************/
 /** to support BOM (e.g. utf-8 bom), 0: disable, 1: enable */
#define INI_ALLOW_BOM 1

/******************************************************************************
 **    VARIABLE DEFINITIONS
 ******************************************************************************/


/******************************************************************************
 **    inner FUNCTION DEFINITIONS
 ******************************************************************************/
static inline bool isCommentTag(char ch)
{
    return (ch == '#' || ch == ';') ? true : false;
}

static IniSection* addScetionWithName(IniSectionMap& secs, const IniKey& name)
{
    return &(secs[name]);
}

static bool iniFileHasSection_l(IniSectionMap& secs, const IniKey& key)
{
    bool ret = true;
    IniSectionMap::iterator it = secs.find(key);

    if (it == secs.end())
    {
        ret = false;
    }    
   
    return ret;
}

static bool iniFileFindValues(IniSectionMap& secs, 
                              const IniKey& skey, /** section key */
                              const IniKey& vkey, /** value key */
                              IniValueArray** vals)
{
    bool ret = false;
    IniSectionMap::iterator it = secs.find(skey);

    if (it != secs.end())
    {
        IniSection& sec = it->second;

        IniSection::iterator vit = sec.find(vkey);

        if (vit != sec.end())
        {
            *vals = &(vit->second);
            ret = true;
        }
    }

    return ret;
}


static const char* iniFileParseSection(std::string& secName,
                                       const char* line, 
                                       const char* line_end,
                                       uint32_t line_num, 
                                       CollieErrc* ret)
{
    const char* ptr = line;
    const char* pEnd = line_end;
    char* section = strchr((char*)ptr, ']');
    /** process section name */

    if (section)
    {
        const char* pLeft = ptr;
        char* pRight = section - 1;

        /** trim left white-space */
        while (isspace(*pLeft) && *pLeft) pLeft++;

        /** trim right white-space */
        while (isspace(*pRight) && pRight >= pLeft) pRight--;

        *(pRight + 1) = '\0';

        secName = pLeft;
        ptr = section + 1; /** + 1 indicate skip character ']' */
        *ret = CollieErrc::OK;
    }
    else
    {
        LOGE("syntax error[line:%d]: section doesn't end with ']'\n", line_num);
        ptr = pEnd;
    }

    return ptr;
}

static const char* iniFileParseParameter(IniKey& skey,
                                         IniValue& sval,
                                         const char* line,
                                         const char* line_end,
                                         uint32_t line_num, 
                                         CollieErrc* ret)
{
    const char* ptr = line;
    const char* pEnd = line_end;

    char* key_end = strchr((char*)ptr, '=');

    if (key_end)
    {
        char* pRight = key_end - 1;

        /** strip right white-space */
        while (isspace(*pRight) && pRight >= ptr) pRight--;

        uint32_t key_len = (uint32_t)((pRight + 1) - ptr);
        const char* key = ptr;

        char* value = key_end + 1; /** + 1 indicate skip character '=' */

        /** strip left white-space */

        while (isspace(*value) && value < pEnd) value++;


        if (value < pEnd)
        {
            /** find value end */
            char* value_end = value;
  
            /** strip ritght white-space and comment
             *  after the value of parameter 
             */
            while ((!isspace(*value_end) &&  (!isCommentTag(*value_end))) &&
                   (value_end < pEnd))
            {
                value_end++;
            }

            /** for support blank space in ini value stirng */
            /** receive value parameter stirng until character '\n'(newline),
             *  '\r'(enter) or comment tag 
             */
            while ((!((*value_end == '\n') || (*value_end == '\r')) && 
                  (!isCommentTag(*value_end))) && (value_end < pEnd))
            {
                value_end++;
            }

            /** strip right white-space*/
            pRight = value_end - 1;
            while (isspace(*pRight) && pRight >= value) pRight--;
            value_end = pRight + 1;
            

            if (value_end > value)
            {
                uint32_t value_len = (uint32_t)(value_end - value);
                
                skey.append(key, key_len);
                sval.append(value, value_len);                
                ptr = value_end;
                *ret = CollieErrc::OK; 
            }
            else
            {
                LOGE("syntax error: the value of parameter "
                     "is empty(right of \'=\')\n");
                ptr = pEnd;
                *ret = CollieErrc::Undefined;
            }

        }
        else
        {
            LOGE("syntax error[line:%d]: the value of parameter "
                 "is empty(right of \'=\')\n", line_num);
            ptr = pEnd;
            *ret = CollieErrc::Undefined;
        }

      
    }
    else
    {
        LOGE("syntax error[line:%d]: not complete key, "
             "doesn't find end character \'=\' \n", line_num);
        ptr = pEnd;
        *ret = CollieErrc::Undefined;
    }

    return ptr;
}


static CollieErrc iniFileParseLine(IniSectionMap& secs,
                                IniSection** curSec,
                                const char* line, 
                                uint32_t length, 
                                uint32_t line_num)
{
    const char* ptr = line;
    const char* pEnd = line + length;
    CollieErrc ret = CollieErrc::OK;

    while (ptr < pEnd)
    {
        if (isspace(*ptr))/** skip white-space  */
        {
            ptr++;
            continue;            
        }

        char ch = *ptr;

        if (isCommentTag(ch))
        {
            /** skip component */
            ptr = pEnd;
        }
        else if (ch == '[')
        {
            IniKey key;
            
            /** ++ indicate skip character '[' */
            ptr = iniFileParseSection(key, ++ptr, pEnd, line_num, &ret);

            if (CollieErrc::OK == ret)
            {  
                if (iniFileHasSection_l(secs, key) == false)
                {
                    *curSec = addScetionWithName(secs, key);
                }
                else
                {
                    LOGE("syntax error[line:%d]: section(%s) already exist,"
                        " end parse \n", line_num, ptr);
                    ptr = pEnd;
                    ret = CollieErrc::Undefined;
                }
        
            }
        }
        else if (ch == '\0') /** line end */
        {
            /**do nothing */
        }
        else
        {/** parse key-value  pair */

            if (secs.size() == 0)
            {/** if havn't sections yet, the create a default section */
                /** alloc default section */
                *curSec = addScetionWithName(secs, DEFAULT_SECTION_NAME);
            }
           
           /**
            * current section aways is the newest section ?
            * TODO, this case is aways right?
            */

            if (curSec)
            {
                IniKey skey;
                IniValue sval;
                
                ptr = iniFileParseParameter(skey, sval, ptr, 
                                            pEnd, line_num, &ret);

                if (CollieErrc::OK == ret)
                {
                    IniValueArray& miva = (**curSec)[skey];

                    miva.push_back(sval);
                }
            }            
        }
    }

    return ret;
}



static CollieErrc iniFileParseFile(IniSectionMap& secs, FILE* fp)
{
    CollieErrc ret = CollieErrc::OK;
   
    uint32_t line_num = 0;
    char line_buf[LINE_BUF_MAX_LEN] = { 0 };
    char* pBuf = line_buf;
    IniSection* curSec = nullptr;

    while (fgets(line_buf, LINE_BUF_MAX_LEN, fp))
    {
        line_num++;
        pBuf = line_buf;
#if INI_ALLOW_BOM
        if ((line_num == 1) && (unsigned char)line_buf[0] == 0xEF &&
            (unsigned char)line_buf[1] == 0xBB &&
            (unsigned char)line_buf[2] == 0xBF) 
        {
            pBuf += 3;
        }
#endif
 
        ret = iniFileParseLine(secs, &curSec, pBuf, (uint32_t)strlen(pBuf), line_num);

        if (CollieErrc::OK != ret)
        {
            LOGE("parse line failed\n");
            break;
        }
    }

    return ret;
}


struct IniFile::impl
{
    bool mIsLoaded = false;
    IniSectionMap mSections;
};

/******************************************************************************
 **    FUNCTION DEFINITIONS
 ******************************************************************************/

IniFile::IniFile()
    :mImpl(new impl)
{
    /** todo somethings */
}

IniFile::~IniFile()
{
     /** todo somethings */  
}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

CollieErrc IniFile::load(const std::string& fileName)
{
    CollieErrc ret = CollieErrc::BadParameter;

    if (!fileName.empty())
    {
        if (!mImpl->mIsLoaded)
        {
            FILE* fp = fopen(fileName.c_str(), "r");

            if (fp)
            {
                ret = iniFileParseFile(mImpl->mSections, fp);

                if (CollieErrc::OK == ret)
                {
                    mImpl->mIsLoaded = true;
                }

                fclose(fp);
            }
            else
            {
                LOGE("open ini file(%s) failed(%d, %s)\n", 
                    fileName.c_str(), errno, strerror(errno));
            }
        }
        else
        {
            LOGE("already load one file, cann't load more\n");
            ret = CollieErrc::InvalidStatus;
        }
    }

    return ret;
}

CollieErrc IniFile::getValue(const IniKey& section,  const IniKey& key, IniValue& val)
{
    CollieErrc ret = CollieErrc::InvalidStatus;

    if (mImpl->mIsLoaded)
    {
        IniValueArray* vals = nullptr;
        if (iniFileFindValues(mImpl->mSections, section, key, &vals))
        {
            IniValueArray& miva = *vals;
            if (miva.size() > 0)
            {
                val = miva[0];
                ret = CollieErrc::OK;
            }
        }
        else
        {
            ret = CollieErrc::NotFound;
        }        
    }
    
    return ret;
}                       
                       
CollieErrc IniFile::getValues(const IniKey& section, const IniKey& key, IniValueArray& vals)
{
    CollieErrc ret = CollieErrc::InvalidStatus;

    if (mImpl->mIsLoaded)
    {
        IniValueArray* miva = nullptr;
        if (iniFileFindValues(mImpl->mSections, section, key, &miva))
        {
            vals = *miva;
            ret = CollieErrc::OK;
        }
        else
        {
            ret = CollieErrc::NotFound;
        }  
    }
    
    return ret;
}                         

CollieErrc IniFile::getSection(const IniKey& key, IniSection& ms)
{
    CollieErrc ret = CollieErrc::InvalidStatus;

    if (mImpl->mIsLoaded)
    {
        IniSectionMap::iterator it = mImpl->mSections.find(key);

        if (it != mImpl->mSections.end())
        {
            ms = it->second;
            ret = CollieErrc::OK;
        }
        else
        {
            ret = CollieErrc::NotFound;
        }
    }
    
    return ret;
}


bool IniFile::hasSection(const IniKey& key)
{
    bool ret = false;
    if (mImpl->mIsLoaded)
    {
        ret = iniFileHasSection_l(mImpl->mSections, key);
    }
    
    return ret;
}

bool IniFile::hasKey(const IniKey& section, const IniKey& key)
{
    bool ret = false;
    if (mImpl->mIsLoaded)
    {
        IniSectionMap::iterator it = mImpl->mSections.find(section);

        if (it != mImpl->mSections.end())
        {
            IniSection& sec = it->second;
            IniSection::iterator vit = sec.find(key);

            if (vit != sec.end())
            {
                ret = true;
            }            
        }
    }
    
    return ret;
}

void IniFile::dump2Str(std::string& str)
{
    if (mImpl->mIsLoaded)
    {
        IniSectionMap::iterator it = mImpl->mSections.begin();
        std::stringstream ss;
        
        for (; it != mImpl->mSections.end(); it++)
        {
            IniSection& sec = it->second;
            ss << "[" << it->first << "]\n";
            
            IniSection::iterator sit = sec.begin();
            for (; sit != sec.end(); sit++)
            {
                IniValueArray& miva = sit->second;
                const IniKey& vkey = sit->first;
                IniValueArray::iterator vit = miva.begin();

                for (; vit != miva.end(); vit++)
                {
                    ss << vkey << " = " << *vit << "\n";
                }
            }
        }

        str = ss.str();
    }
}

} /** namespace core */
} /** namespace collie */
